classdef dotsTheMessenger < dotsAllSingletonObjects
    % @class dotsTheMessenger
    % Singleton to handle socket communications.
    % @details
    % dotsTheMessenger uses an instance of dotsAllSocketObjects to do
    % communications between Matlab instances (or looped-back
    % communictaions to the same Matlab instance).  It adds behaviors for
    % acknowledging received messages and resending unacknowledged
    % messages.  It also allows waiting for a message while carrying out an
    % arbitrary concurrent behavior.
    %
    % @ingroup foundation
    
    properties
        % topsGroupedList of info about opened sockets
        socketsInfo;
        
        % any function that returns the current time as a number
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        clockFunction;
        
        % fevalable cell array which yields a short time time to the OS or
        % Matlab
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        waitFevalable;
        
        % default duration to wait for a message or acknowledgement
        % @details
        % messageTimeout is the default time to wait for:
        %   - a message to arrive during waitForMessageAtSocket() before
        %   returning with no message
        %   - an acknowledgement to arrive (if necessary) during
        %   sendMessageFromSocket()
        %   .
        % messageTimeout should be in the same units as clockFunction.
        % @details
        % Automatically gets the machine-specific default from
        % dotsTheMachineConfiguration.
        messageTimeout;
        
        % repeat interval between send, unacknowledged messages
        % @details
        % sendMessageFromSocket() may resend messages periodically, until
        % they are acknowledged.  resendInterval should be in units of
        % clockFunction.
        % @details
        % Automatically gets the machine-specific default from
        % dotsTheMachineConfiguration.
        resendInterval;
        
        % class name of a dotsAllSocketObjects subclass
        % @details
        % Instantiates an instance of socketClassname to use for opening
        % and closing sockets, and sending and receiving messages.  The
        % named dotsAllSocketObjects subclass should be appropriate for the
        % local hardware, operating system, etc.
        % @details
        % Automatically gets the machine-specific default from
        % dotsTheMachineConfiguration.
        socketClassname;
        
        % an instance of socketClassname
        % @details
        % The instance of socketClassname that dotsTheMessenger is using
        % for opening and closing sockets, and sending and receiving
        % messages.
        socketObject;
        
        % string IP address that could be used for a client-side socket
        % @details
        % Automatically gets the machine-specific default from
        % dotsTheMachineConfiguration.
        defaultClientIP;
        
        % string IP address that could be used for a server-side socket
        % @details
        % Automatically gets the machine-specific default from
        % dotsTheMachineConfiguration.
        defaultServerIP;
        
        % port number that could be used for a client-side socket
        % @details
        % Automatically gets the machine-specific default from
        % dotsTheMachineConfiguration.
        defaultClientPort;
        
        % port number that could be used for a server-side socket
        % @details
        % Automatically gets the machine-specific default from
        % dotsTheMachineConfiguration.
        defaultServerPort;
        
        % status code indicating a send message was never acknowqledged
        notAcknowledgedStatus = -111;
        
        % status code indicating no message was received
        notReceivedStatus = -222;
        
        % status code indicating timeout waiting for message
        timedOutStatus = -333;
    end
    
    properties(Hidden)
        % 1-byte prefix for the previous sent message
        sentPrefix;
        
        % 1-byte prefixs for the previous received message at each socket
        receivedPrefix;
        
        % roll-over value for 1-byte message prefixes
        % @details
        % prefixModulus wants to be 256, but because of Matlab clips
        % integers rather than rolling them over, 255 is easier to work
        % with.  Consider that 255+1 equals 255, not 0.
        prefixModulus = 255;
    end
    
    methods (Access = private)
        % Constructor is private.
        % @details
        % dotsTheMessenger is a singleton object, so its constructor is not
        % accessible.  Use dotsTheMessenger.theObject() to access the
        % current instance.
        function self = dotsTheMessenger
            dotsTheMachineConfiguration.applySettingsToObject(self, true);
            self.initialize;
        end
        
        % Keep track of the last message prefix received at a socket.
        function setSocketReceivedPrefix(self, sock, prefix)
            ii = sock+1;
            if ii > 0
                self.receivedPrefix(ii) = prefix;
                %disp(sprintf('set receivedPrefix(%d) = %d', ...
                %    ii, self.receivedPrefix(ii)))
            end
        end
        
        % Get the last message prefix received at a socket.
        function prefix = getSocketReceivedPrefix(self, sock)
            ii = sock+1;
            if ii > 0 && ii <= length(self.receivedPrefix)
                prefix = self.receivedPrefix(ii);
            else
                prefix = 0;
            end
        end
    end
    
    methods (Static)
        % Access the current instance.
        function obj = theObject
            persistent self
            if isempty(self) || ~isvalid(self)
                constructor = str2func(mfilename);
                self = feval(constructor);
            end
            obj = self;
        end
        
        % Restore the current instance to a fresh state.
        function reset(varargin)
            factory = str2func([mfilename, '.theObject']);
            self = feval(factory);
            self.set(varargin{:});
            self.initialize;
        end
        
        % Launch a graphical interface to summarize open sockets.
        function g = gui
            self = dotsTheMessenger.theObject;
            g = self.socketsInfo.gui;
        end
    end
    
    methods
        % Restore the current instance to a fresh state.
        function initialize(self)
            if isobject(self.socketObject)
                self.socketObject.closeAll;
            end
            self.initializeLists({'socketsInfo'});
            
            dotsTheSwitchboard.sharePropertyOfObject('clockFunction', self);
            dotsTheSwitchboard.sharePropertyOfObject('waitFevalable', self);
            dotsTheSwitchboard.sharePropertyOfObject('socketClassname', self);
            
            self.socketObject = feval(self.socketClassname);
            self.socketObject.closeAll;
            
            self.sentPrefix = uint8(0);
            self.receivedPrefix = uint8(0);
        end
        
        % Send a message from the given socket.
        % @param msg a variable to send as a message.
        % @param sock a numeric identifier for a socket, as returned by
        % openSocket()
        % @param mustAcknowledge true or false, whether to wait for the
        % recipient to send back an acknowledgement of @a msg.  Defaults to
        % false.
        % @param timeout optional duration to wait for a message
        % acknowledgement (if @a mustAcknowledge is true). Defaults to
        % messageTimeout.  Should be in units of clockFunction.
        % @details
        % Converts @a msg to an array of bytes and sends the bytes from @a
        % sock, to whatever address and port were specified in the call to
        % openSocket().  @a msg must belong one of the following variable
        % types:
        %   - double
        %   - char
        %   - logical
        %   - cell
        %   - struct
        %   - function_handle
        %   .
        % If @a msg is a cell or struct, it must compose elements of these
        % types.  Nested cells and structs are supported.  Any @a msg or
        % element that is an array, including cell arrays, should be at
        % most two-dimensional (mxn).  struct arrays should be
        % one-dimenstional (1xn) but may have any number of fields.
        % @details
        % mustAcknowledge is true, blocks until receiving an
        % acknowledgement from the recipient, or until @a timeout has
        % elapsed.  As long as there has been no acknowledgement,
        % periodically resends @a msg according to resendInterval.
        % @details
        % Returns a status code.  May return notAcknowledgedStatus if the
        % message was sent by never acknowledged (if necessary). Other
        % negative status indicates an error and that @a msg may not have
        % been sent.
        % @details
        % Also returns as a second output the time it took for the
        % recipient to send back an acknowledgement, in units of
        % clockFunction.  If @a msg was never acknowledged and the full @a
        % timeout elapsed, returns negative-@a timeout.  If mustAcknowledge
        % is false, returns zero.
        function [status, ackTime] = sendMessageFromSocket( ...
                self, msg, sock, mustAcknowledge, timeout)
            if nargin < 4
                mustAcknowledge = false;
            end
            
            if nargin < 5
                timeout = self.messageTimeout;
            end
            
            ackTime = 0;
            [bytes, status] = mxGram('mxToBytes', msg);
            if status >= 0
                % attach a new 1-byte prefix to the message
                prefix = mod(1+self.sentPrefix, self.prefixModulus);
                self.sentPrefix = prefix;
                prefixedBytes = cat(2, prefix, bytes);
                status = self.socketObject.writeBytes(sock, prefixedBytes);
                %disp(sprintf('sent from %d with prefix %d:', sock, prefix))
                %disp(sprintf('sent %d[%d]', ...
                %    prefix, numel(prefixedBytes)));
                
                if mustAcknowledge
                    cf = self.clockFunction;
                    interval = self.resendInterval;
                    startTime = feval(cf);
                    nowTime = startTime;
                    endTime = startTime + timeout;
                    while status >= 0 && nowTime < endTime
                        % check for ack, up to the resend interval
                        resendTime = nowTime + interval;
                        while nowTime < resendTime
                            nowTime = feval(cf);
                            if self.socketObject.check(sock);
                                ack = self.socketObject.readBytes(sock);
                                if numel(ack) == 1 && ack == prefix
                                    ackTime = nowTime - startTime;
                                    %disp(sprintf('got %d', ack))
                                    return;
                                    %else
                                    %    disp(sprintf('expected %d, got %d[%d]', ...
                                    %        prefix, ack(1), numel(ack)))
                                end
                            end
                        end
                        
                        % resend and keep waiting the ack
                        status = self.socketObject.writeBytes( ...
                            sock, prefixedBytes);
                        %disp('resent...')
                    end
                    
                    % never got the ack
                    status = self.notAcknowledgedStatus;
                    ackTime = -timeout;
                    %disp('never acknowledged.')
                end
            end
        end
        
        % Receive any message that arrived at the given socket.
        % @param sock a numeric identifier for a socket, as returned by
        % openSocket()
        % @details
        % If a message has arrived at @a sock, returns the message
        % variable. Otherwise, returns an empty array.
        % @details
        % Also returns a status code as a second output.  May return
        % notReceivedStatus of no message was available.  May return other
        % negative status codes if there was an error reading from @sock or
        % decoding message bytes into a variable.
        function [msg, status] = receiveMessageAtSocket(self, sock)
            prefixedBytes = self.socketObject.readBytes(sock);
            if isa(prefixedBytes, 'uint8') && ~isempty(prefixedBytes)
                % got a message
                prefix = prefixedBytes(1);
                
                %disp(sprintf('received %d[%d]', ...
                %    prefix, numel(prefixedBytes)));
                
                %disp(sprintf('  vs. prefix %d', ...
                %    self.getSocketReceivedPrefix(sock)))
                if numel(prefixedBytes) > 1 ...
                        && prefix ~= self.getSocketReceivedPrefix(sock)
                    % a new, substantial message,
                    %   acknowledge it, twice, for good measure (?)
                    %   decode it
                    self.setSocketReceivedPrefix(sock, prefix);
                    status = self.socketObject.writeBytes(sock, prefix);
                    status = self.socketObject.writeBytes(sock, prefix);
                    [msg, status] = mxGram('bytesToMx', ...
                        prefixedBytes(2:end));
                    % if status > 0
                    %     disp(sprintf('received: %s[%d]', ...
                    %         class(msg), numel(msg)));
                    % end
                    
                else
                    % an old or insubstantial message
                    %   recur until no message or a substantial message
                    %disp('recurring...');
                    [msg, status] = self.receiveMessageAtSocket(sock);
                end
                
            else
                % no message
                msg = [];
                status = self.notReceivedStatus;
            end
        end
        
        % Block and check the given socket for an arriving message.
        % @param sock a numeric identifier for a socket, as returned by
        % openSocket()
        % @param timeout optional duration to wait for a message to arrive.
        % Defaults to messageTimeout.  Should be in units of clockFunction.
        % @param waitFevalable optional fevalable cell array to yield CPU
        % cycles while waiting.  Defaults waitFevalable.
        % @details
        % Blocks for the specified @a timeout, checking for messages
        % arriving at @a sock and allowing for concurrent behaviors with @a
        % waitFevalable.  waitFevalable could yield CPU cycles to the
        % operating system, for example.
        % @details
        % If a message arrives at @a sock during the specified interval (or
        % had already arrived), returns the message variable immediately.
        % If no message arrives, returns an empty array after waiting the
        % full interval.
        % @details
        % Returns a status code.  If no message arrives, returns
        % timedOutStatus.  Other negative status codes indicates an error
        % and probably failure to decode @a msg from bytes to a variable.
        function [msg, status] = waitForMessageAtSocket( ...
                self, sock, timeout, waitFevalable)
            
            if nargin < 3
                timeout = self.messageTimeout;
            end
            
            if nargin < 4
                wf = self.waitFevalable;
            else
                wf = waitFevalable;
            end
            
            cf = self.clockFunction;
            endTime = feval(cf) + timeout;
            
            gotMsg = self.socketObject.check(sock);
            stillTime = true;
            while gotMsg==0 && stillTime
                stillTime = feval(cf) < endTime;
                feval(wf{:});
                gotMsg = self.socketObject.check(sock);
            end
            if stillTime
                [msg, status] = self.receiveMessageAtSocket(sock);
            else
                msg = [];
                status = self.timedOutStatus;
            end
            %disp(sprintf('arrived at %d:', sock)),msg
        end
        
        % Open a socket for communicating via Ethernet and UDP.
        % @param localIP string IP address for this machine
        % @param localPort integer port number to go with the local IP
        % address
        % @param remoteIP string IP address for this or another machine
        % @param remotePort integer port number to go with the remote IP
        % address
        % @details
        % Opens an Ethernet UDP connection between ports at two IP
        % addresses.  The IP addresses may be the same or different.  @a
        % localIP should be the address of this machine, and may be the
        % "loopback" address, '127.0.0.1'.  The port numbers must be
        % different.
        % @details
        % Returns a numeric identifier for the new UDP connection, suitable
        % for use with sendMessageFromSocket(), receiveMessageAtSocket(),
        % and waitForMessageAtSocket();
        % @details
        % Stores information about each opened socket in socketsInfo
        function sock = openSocket(self, localIP, localPort, remoteIP, remotePort)
            sock = self.socketObject.open( ...
                localIP, localPort, ...
                remoteIP, remotePort);
            
            if sock >= 0
                self.socketsInfo.addItemToGroupWithMnemonic( ...
                    localIP, sock, 'local IP')
                self.socketsInfo.addItemToGroupWithMnemonic( ...
                    remoteIP, sock, 'remote IP')
                self.socketsInfo.addItemToGroupWithMnemonic( ...
                    localPort, sock, 'local port')
                self.socketsInfo.addItemToGroupWithMnemonic( ...
                    remotePort, sock, 'remote port')
                
                self.setSocketReceivedPrefix(sock, 0);
                
            else
                warning('%s failed(%d) to open socket from %s:%d to %s:%d', ...
                    class(self), sock, ...
                    localIP, localPort, ...
                    remoteIP, remotePort);
            end
            
            % disp(sprintf('%s, %d, %s, %d ...%d', ...
            %     localIP, localPort, remoteIP, remotePort, sock));
        end
        
        % Close a socket.
        % @param sock a numeric identifier for a socket, as returned by
        % openSocket()
        % @details
        % Frees the resources associeted with @a sock.
        % @details
        % Returns a status code.  A negative status code probably indicates
        % that @a sock is not a valid socket identifier.
        function status = closeSocket(self, sock)
            if isempty(sock) || sock < 0
                status = 0;
            else
                status = self.socketObject.close(sock);
                self.setSocketReceivedPrefix(sock, 0);
            end
        end
        
        % Clear any data messages and inernal accounting for a socket.
        % @param sock a numeric identifier for a socket, as returned by
        % openSocket()
        % @details
        % Reads from @a sock until it has no queued messages and resets
        % dotsTheMessenger's internal accounting for @a sock, but does not
        % close the @a sock.
        % @details
        % Returns a status code.  A negative status code probably indicates
        % that @a sock is not a valid socket identifier.
        function status = flushSocket(self, sock)
            status = 0;
            try
                while self.socketObject.check(sock);
                    msg = self.socketObject.readBytes(sock);
                end
            catch err
                status = -1;
            end
            self.setSocketReceivedPrefix(sock, 0);
        end
        
        % Open a socket with some default "server" properties.
        function sock = openDefaultServerSocket(self)
            sock = self.openSocket( ...
                self.defaultServerIP, self.defaultServerPort, ...
                self.defaultClientIP, self.defaultClientPort);
        end
        
        % Open a socket with some default "client" properties.
        function sock = openDefaultClientSocket(self)
            sock = self.openSocket( ...
                self.defaultClientIP, self.defaultClientPort, ...
                self.defaultServerIP, self.defaultServerPort);
        end
    end
end